test(plugin-hono-server): pin the UI auto-discovery block end to end - #16253
Conversation
The block in `HonoServerPlugin.start()` reads `type`, `staticPath` and `slug`
off every loaded plugin and mounts `/slug` plus `/slug/*` for the ones that
answer. Grepping `staticPath` across every `.ts` outside `node_modules`/`dist`
returned four lines -- the `PluginSchema` declaration and the three reads inside
the block itself -- and `slug` had the same shape. Zero producers, no test: the
block served only externally authored plugins, and nothing in the tree would
have noticed if it stopped working.
A block in that state is indistinguishable from dead code to anyone reading the
repository, which is what it already cost: a careful reader concluded the legacy
arm was unreachable and the premise had to be falsified by a purpose-built probe
driving the real kernel. This is that probe, made permanent.
Four pins, three of them landed:
A -- `ObjectKernel.use()` accepts a `ui` plugin and stores `type`,
`staticPath` and `slug` verbatim into `kernel.plugins`.
B -- the modern `ui` arm mounts `/slug` and `/slug/*`, for an explicit slug
and for one derived from the plugin name.
C -- the legacy `ui-plugin` arm: a documented placeholder, because the two
candidate shapes are incompatible and the ruling picks between them.
D -- the negative control: same fixture, same existing static root, one key
different, mounts nothing. Without D, B proves nothing.
The fixture is registered through the real kernel and the real `init()`/`start()`
run against the context the kernel hands its plugins. No product behaviour is
changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract review (clause ②) — PASS, with one round this seat is asking for before landingReviewed by an isolated subagent at the contract-review tier ( Clause ② All three of the PR's ablations reproduced exactly, restored to the same HEAD blobs the implementer reported. ⭐ The finding: four mutations leave every pin greenThe review ran five of its own beyond the PR's three. Four of them changed real route behaviour and no pin noticed:
The verdict on the hazard is still clean: "no landed pin is a reading that cannot fail — every pin, as titled, reddens under a mutation of the thing it names, and the pins discriminate." The gap is the title, not the pins: ⇒ ⛔ This is not overturning the PASS. The review offered exactly this as an option — "if the seat wants it before landing" — and choosing among the options it left open is the dispatching seat's call. For the maintainer — above the review floor, escalated not decided
The review notes this is the repo's usual pin-then-change discipline, but that it contradicts the standing triage ruling on this card (land A/B/D, leave C a placeholder, do not guess), so it stopped rather than overturning it. ⛔ Not this seat's either. Recorded for the provenance line
Two corrections carried: triage's "6 lines" for
Generated by Claude Code |
Contract review found the landed pins blind past the route string: because
A, B and D replace `rawApp.get` with a recorder, no handler is ever installed
and nothing is served, so four mutations that change real route behaviour left
every pin green -- `root` swapped to `process.cwd()`, `rewrite` flipped to
false, the type guard widened to `!== 'driver'`, and the default redirect
disabled.
A pin file whose title promises more than it measures is the same defect this
card exists to close, one turn later: a new artifact a reader over-trusts. So:
E (new) -- leaves `rawApp.get` alone, installs the real handlers on the real
Hono app, and drives `rawApp.request(...)`. Two requests: the base route
must answer with the fixture's own index bytes, and an asset request must
answer with the asset -- the second is what pins the prefix strip, since
without it the SPA fallback returns index.html with a 200 and only the
BODY tells the two apart.
D (widened) -- now every declared plugin type except `ui`, derived from
`CORE_PLUGIN_TYPES` rather than listed, so the guard's specificity is
pinned and a type added tomorrow is covered without anyone remembering.
Header -- says what each group actually observes, registration vs response,
and names the `default`/`isDefault` redirect as still unpinned.
The `default` redirect is deliberately NOT pinned: the fixture does not set it
and that is a wider change than this card carries. Pin C is untouched -- the
#15638 deferral stands exactly as it was.
Test file only. No product code changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
⛔ The one case this seat prescribed would itself have been blind — corrected by measurementHead This seat relayed the review's cheapest-repair suggestion verbatim: one case leaving
⇒ ⭐ A case that appears to cover
⇒ the prescription came from the review and passed through this seat unexamined. Both of us owed that drive; the implementer did it. What landedCase E, in two requests — real handlers installed on the real Hono app, asserting on the response body: The second negative control went wider than offered. Rather than one non- On the wording it took the harder option: the new case makes "end to end" true, so the title stands — and the body now states plainly that A/B/D pin which routes are registered and are blind downstream of the route string, while E installs the real handlers and asks the real app for a response. A new section names the ⭐ It also superseded its own earlier report comment (id Unchanged and verified: clause ② still Package suite at the new head: ⇒ A delta review is dispatched, scoped to Generated by Claude Code |
Delta contract review at
|
Delta review found the same shortcut one level down, inside the repair. The
mount hard-codes three properties -- root, rewrite, spa -- and E covered two:
with `rewrite` on, the base route is rewritten to `/`, which resolves to the
mount directory, so the STATIC handler serves index.html itself and the SPA
fallback is never reached. Retargeting the fallback at a file that does not
exist left all thirteen cases green -- including the one titled "serves the SPA
index for the base route".
A case TITLED for a property it does not exercise is this card's own defect
reproduced inside the artifact meant to cure it, so the fix is a real falsifier
rather than a rename:
- new case -- request a deep client-side route that matches no file on disk,
so the static handler passes and the scoped SPA fallback is the only thing
that can answer. This is the one case that exercises `spa: true`.
- the base-route case is retitled to what it actually does (serves the index
from the mounted directory) and now owns `root` alone.
- E's docblock states the three properties, which case owns each, and the
named falsifier for each -- a coverage claim per property is worth exactly
what its falsifier is.
- the base-route assertion tightens from toContain to toBe; the response is
byte-exact, so the looser matcher was one incidental pass.
Test file only. No product code changed. Pin C untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Landing provenance —
|
Part of #16050 — this lands the executable half. See "What is deliberately not here" below for the half that is not executable yet.
The gap
HonoServerPlugin.start()auto-discovers UI plugins: it readstype,staticPathandslugoff every plugin the kernel has loaded and mounts/slugplus/slug/*for the ones that answer. Re-derived on this branch's merge base:staticPath,.tsonly, excludingnode_modules/distPluginSchemadeclaration plus the three reads inside the block itselfstaticPath, all tracked files, same exclusionsauthorable-surfacebaselines, and three prose lines (one changeset, two docs pages)slugas akernel/PluginkeyProducers: zero, on either population. Tests: none. So the block mounted real routes for a population living entirely outside this repository, and nothing inside it would have noticed if the block stopped working.
That is not merely untested. A block in that state is indistinguishable from dead code to anyone reading this repo, and that has already cost a round: on #15638 a careful reader concluded the legacy arm was unreachable, and the premise had to be falsified by a purpose-built probe driving the real kernel. This PR is that probe, made permanent.
What lands
One new file,
packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts. The fixture plugin goes through the realObjectKernel.use(), and the realHonoServerPlugin.init()/start()run against the very context objectbootstrap()hands its plugins. Nothing stubs the kernel, the plugin, or the branch under test.The groups observe two different things, and the file says so. A, B and D replace
rawApp.getwith a recorder: they pin which routes are registered, and are blind to everything downstream of the route string. E leavesrawApp.getalone, so the real handlers install on the real Hono app, and asks it for a response.kernel.use()accepts auiplugin and storestype,staticPathandslugverbatim.PluginLoader.toPluginMetadatais a cast, not a copy, which is exactly what makes the block reachable at all.uiarm registers/slugand/slug/*, for an explicit slug and for one derived from the plugin name. Pinned as the exact registration sequence, two per route (static handler, then scoped SPA fallback), because a set comparison would hide the loss of the fallback.ui, derived fromCORE_PLUGIN_TYPESrather than listed, so the guard's specificity is pinned and a type added tomorrow is covered without anyone remembering. A further case covers the other conjunct of the same guard, auitype with nostaticPath.root: plugin.staticPathrootelsewhererewrite: true/slug/assets/app.cssanswers with the assetrewrite: falsespa: truerewriteon, the base route is rewritten to/, which resolves to the mount directory, so the static handler servesindex.htmlitself and the SPA fallback is never reached. A base-route case therefore says nothing whatsoever aboutspa— it passes with the fallback completely broken. Only a path matching no file on disk reaches it. The base-route case is titled for what it actually does, and the SPA claim lives on the case that can fail for it.D is not decoration. A harness that registered everything would produce B's four registrations whether or not the branch works, so without D, B proves nothing.
Each pin is shown capable of failing
The defect this card is about is one level up: a reading that cannot fail. So each pin was driven red by mutating product code, and each mutation restored — mutation confirmed on disk by counting the removed and injected text, restore proven by
git diff HEADempty plus agit hash-objectmatch against the HEAD blob. Both mutated files are consumed from source by this package's vitest config (@objectstack/coreis aliased topackages/core/src/index.ts;hono-plugin.tsis a relative import), so no rebuild sits between the mutation and the reading.Every row below was re-measured at
16d0f23bcb6, the current head — not carried over from an earlier tree. The file has 14 cases plus onetodo.hono-plugin.ts:plugin.type === 'ui'becomes'ui-ABLATED'hono-plugin.ts: type guard dropped, leavingif (plugin.staticPath)hono-plugin.ts: guard widened toplugin.type !== 'driver'driverdriveralone, A, B, Ehono-plugin.ts:root: plugin.staticPathbecomesroot: process.cwd()hono-plugin.ts:rewrite: truebecomesfalsehono-plugin.ts: SPA fallback retargeted tomissing.htmlexpected 404 to be 200)plugin-loader.ts:toPluginMetadatacopies a whitelist instead of castingThree things to read off it. The B and D mutations are complementary — one reddens B alone, another D alone — which is how you know the pins discriminate rather than moving together. Rows 4, 5 and 6 are the reason E exists: each changes what the server actually serves, and every registration-only pin sits green through all three. And rows 5 and 6 are why E needs three requests rather than one: with
rewriteoff the base route still returns200with the index because the fallback catches it, and with the fallback broken the base route still returns200because the static handler answers — each property is invisible to the other two cases.What the pins deliberately do NOT observe
The
default/isDefaultredirect, which mounts/at the plugin's base route. The fixture does not set it and pinning it is a wider change than this card carries. The file's header names it as unpinned so the next reader does not over-trust the file — which is the same failure mode this card exists to close, one level up.What is deliberately not here
Pin C, the legacy
ui-pluginarm, is a documentedit.todoplaceholder. The block matchesplugin.type === 'ui' || plugin.type === 'ui-plugin', and the second disjunct is the subject of #15638, which is with the maintainer. The two candidate rulings need incompatible pins — under "remove", aui-pluginfixture must register nothing; under "declare/convert", it must be normalised toui, register, and warn — so writing either now would pin a guess. The placeholder states both shapes and names the card. Whoever lands #15638 writes the case there; the harness takes it unchanged, only the fixture'stypeand the expectation differ. #15638 stays open and is not addressed by this PR.Also out of scope, and neither folded in nor touched here: #16049 (the
PluginSchemazero-runtime-callers finding) and #16047 (the enforce-at-producer route). Same origin batch, different cards; both stay open.No product behaviour changes. Test file only.
Verification
All readings at
16d0f23bcb6, on a tree verified clean bygit status --shortafter every ablation, with both mutated files confirmed byte-identical to their HEAD blobs.pnpm --filter @objectstack/plugin-hono-server test—Test Files 23 passed (23)·Tests 261 passed | 1 todo (262). The pin file alone:14 passed | 1 todo (15).pnpm --filter @objectstack/plugin-hono-server typecheck— exit 0, includingcheck:test-typecheck(0 file(s) / 0 error(s)). The new file is confirmed present in two tsc programs by--listFiles(tsconfig.jsonandtsconfig.test.json), so that green is a statement about it and not around it.Reconciliation — 46 famil(ies)line, command set byte-identical to the sets derived at both earlier heads. Re-run in full: 44 green, 0 red; 2 reportPREREQUISITE NOT METat exit 3 (check:dual-build-cjs-loads,check:type-check-debt) because this worktree has no full workspace build — not measured, and neither pass nor finding. A further 6 families are marked NOT MEASURED by the tool itself: their argv takes a value that exists only inside a CI run.CORE_PLUGIN_TYPESfrom@objectstack/spec/kernel) clearscheck:test-source-alias,check:undeclared-dep-importsandcheck:cross-package-test-inputs, all exit 0.packages/were run rather than assumed silent:check:authz-resolver,check:error-code-casing,check:filter-alias-parity, all exit 0.Changeset
None, and the
skip-changesetlabel instead. The diff adds one test file;tsup's entry issrc/index.tsand the package'sfilesis["dist","README.md","CHANGELOG.md"], so nothing in it reaches the published artifact. That is the labelled population exactly: a diff that publishes nothing from any released package. Apatchhere would announce a change to consumers who receive no change.🤖 Generated with Claude Code
https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N